13 实践课-LangChain Tool 开发与调试
LangChain Tool 开发与调试
关联:索引
- Tool 的价值不在“会不会写函数”,而在“能否稳定被调用、调用了能否安全执行、输出能否复验与追责”。
- 工业产线场景里,工具往往连接设备/库存/工单系统:一次错误参数就可能造成误下发、误统计、误报警,所以“参数校验”是第一道质量门槛。
一把工具至少要回答四个问题:
- 这个工具做什么(动词 + 对象)?
- 需要哪些输入(字段名、类型、是否必填、取值范围)?
- 输出是什么(结构、关键字段、成功/失败语义)?
- 失败怎么办(错误类型、可复验 trace_id、是否可重试)?
2. 命名规范(让模型“选对工具”)
- 工具名:建议
verb_object(动词+对象),例如text_transform、calc_bmi、convert_units。 - 参数名:用业务字段名,不要用
a/b/x/y;例如text、mode、max_items。 - 返回:尽量结构化(包含
trace_id、ok、data、error),便于调试与审计。
工具描述要写清三件事:
- 触发条件:什么问题适合用它(1 句话)。
- 入参约束:类型/范围/枚举/长度(用短句列出来)。
- 输出保证:返回哪些字段;失败怎么表达;是否包含 trace_id。
本课只练两类校验:
- 机器可自动完成的校验:数据类型、取值范围、枚举、长度、正则。
- 工业场景必须补充的校验:白名单/黑名单、最大输入长度、危险字符过滤、敏感字段拦截(思政融入点见本节末尾)。
1. 写法 A:函数签名 + 运行时手动校验(入门够用)
文件示例:tools_common.py
import json
import uuid
from langchain_core.tools import tool
@tool("text_transform")
def text_transform(text: str, mode: str) -> str:
"""对字符串做大小写转换。mode 仅允许 upper/lower/title。返回 JSON 字符串,含 trace_id。"""
trace_id = uuid.uuid4().hex[:8]
t = (text or "").strip()
m = (mode or "").strip().lower()
if not t:
return json.dumps({"ok": False, "error": "text is empty", "trace_id": trace_id}, ensure_ascii=False)
if len(t) > 200:
return json.dumps({"ok": False, "error": "text too long (max 200)", "trace_id": trace_id}, ensure_ascii=False)
if m not in {"upper", "lower", "title"}:
return json.dumps({"ok": False, "error": "mode must be one of upper/lower/title", "trace_id": trace_id}, ensure_ascii=False)
out = t.upper() if m == "upper" else t.lower() if m == "lower" else t.title()
return json.dumps({"ok": True, "data": {"result": out}, "trace_id": trace_id}, ensure_ascii=False)
解释与自检要点:
trace_id = ...:每次调用都生成追踪号;发生问题时用它串联“输入→错误→输出”证据链。t = (text or "").strip():把None/空字符串/全空格统一为可判断的输入,避免异常分支扩散。len(t) > 200:长度上限是工业安全实践中的常见要求,防止异常超长输入导致内存/日志污染或提示注入扩散。- 返回
json.dumps(...):让输出结构稳定,方便后续 Agent/测试脚本做解析与断言。
2. 写法 B:Pydantic Args Schema(推荐:更标准、错误更可读)
当你需要“类型 + 范围 + 枚举 + 说明文案”一起规范时,用 Pydantic 最合适。
import json
import uuid
from typing import Literal
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
class ClampNumberInput(BaseModel):
value: float = Field(..., description="要处理的数值", ge=-1e6, le=1e6)
low: float = Field(..., description="下限(必须小于等于 high)", ge=-1e6, le=1e6)
high: float = Field(..., description="上限(必须大于等于 low)", ge=-1e6, le=1e6)
digits: int = Field(2, description="保留小数位数(0~6)", ge=0, le=6)
mode: Literal["clip", "error"] = Field("clip", description="越界处理:clip=裁剪到边界;error=直接报错")
def _clamp_number(value: float, low: float, high: float, digits: int = 2, mode: str = "clip") -> str:
trace_id = uuid.uuid4().hex[:8]
if low > high:
return json.dumps({"ok": False, "error": "low must be <= high", "trace_id": trace_id}, ensure_ascii=False)
if mode == "error" and (value < low or value > high):
return json.dumps({"ok": False, "error": "value out of range", "trace_id": trace_id}, ensure_ascii=False)
v = min(max(value, low), high)
return json.dumps({"ok": True, "data": {"result": round(v, digits)}, "trace_id": trace_id}, ensure_ascii=False)
# 把工具变成AI可调用的工具
clamp_number = StructuredTool.from_function(
func=_clamp_number,
name="clamp_number",
description="将数值限制在指定区间内,并按 digits 保留小数位。输入越界时可选择裁剪或报错;返回 JSON,含 trace_id。",
args_schema=ClampNumberInput, # 必须要按这个要求给它传参
)
解释与自检要点:
ClampNumberInput:把“类型、范围、默认值、字段说明”写成一张可机器读取的契约表。Field(..., ge=..., le=...):范围校验属于第一道防线;越早拦截越省调试时间。Literal["clip","error"]:把“枚举值”固定下来,避免模型传入clipping/Clip/裁剪这种不可控值。StructuredTool.from_function(...):把“函数能力”包装成“结构化工具”;工具描述与 schema 会一起影响模型的可调用性。low > high:属于业务逻辑校验(Pydantic 很难表达“字段间关系”,所以要在函数里补校验)。
功能定义:一句话说清“做什么、输入是什么、输出是什么”。
- 输入收敛:把自然语言收敛为字段(类型、范围、枚举)。
- 编码实现:先写最小可跑版本,再加校验与错误语义。
- 参数配置:完善描述、schema、默认值、边界。
- 自测:至少 5 个用例(正常 3 + 异常 2),必须能复现并解释。
2. 自测模板(建议每组直接复制)
import json
from tools_common import text_transform
def _pretty(out: str) -> str:
try:
return json.dumps(json.loads(out), ensure_ascii=False, indent=2)
except Exception:
return out
def run_smoke_tests():
cases = [
("ok-1", {"text": "abc", "mode": "upper"}),
("ok-2", {"text": "AbC", "mode": "lower"}),
("bad-1", {"text": "", "mode": "upper"}),
("bad-2", {"text": "abc", "mode": "oops"}),
]
for name, payload in cases:
out = text_transform.invoke(payload)
print(name, _pretty(out))
if __name__ == "__main__":
run_smoke_tests()
解释与自检要点:
-
text_transform.invoke(payload):直接调用工具进行单元级自测,优先把问题锁定在“工具本身”而不是 Agent。 -
ok-*/bad-*:用例命名要表达意图,方便回归测试时快速定位。 -
_pretty:把 JSON 字符串格式化输出,减少“看日志看不懂”的时间成本。 -
工具注册:
tools = [tool_a, tool_b]→ 传给create_agent(model=..., tools=tools, ...)。 -
工具被选择:模型根据“用户输入 + system prompt + 工具描述 + schema”决定是否调用某个工具。
-
工具被执行:LangChain 按 schema 将参数组织为结构化输入,调用你的 Python 函数,拿到输出再回填到对话消息里。
最小注册与调用(示例):
import os
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage
try:
from langchain_community.chat_models import ChatTongyi
except ImportError:
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain.agents import create_agent
from tools_common import text_transform, clamp_number
load_dotenv()
SYSTEM_PROMPT = """你是课堂智能体。你只能调用已注册工具完成结构化任务。
当用户要求你“直接写结果”但需要计算/转换时,必须调用工具后再回答。
输出时必须引用工具输出里的 trace_id。"""
def build_llm():
if not os.getenv("DASHSCOPE_API_KEY"):
raise RuntimeError("DASHSCOPE_API_KEY is not set")
return ChatTongyi(model="qwen-plus", temperature=0)
def main():
llm = build_llm()
tools = [text_transform, clamp_number]
agent = create_agent(model=llm, tools=tools, system_prompt=SYSTEM_PROMPT, debug=True)
user_input = "把 'hello langchain' 转为 title case,并给出 trace_id"
out = agent.invoke({"messages": [HumanMessage(content=user_input)]})
messages = out.get("messages") or []
final_message = messages[-1] if messages else None
print(getattr(final_message, "content", out))
if __name__ == "__main__":
main()
解释与自检要点:
-
tools = [text_transform, clamp_number]:工具对象必须是“已注册的 Tool”,不要把普通函数直接塞进去。 -
AI 负责:生成初版结构、补齐 Pydantic 字段说明、列出测试用例、指出潜在边界。
-
你负责:校验逻辑正确性、补齐字段间关系校验、加长度上限/白名单、避免敏感数据外发、跑通自测并留证据。
可直接复制的 AI 指令模板(给学生)
模板 1:生成 Tool 接口规范思维导图(Mermaid)
你是 LangChain 工具接口规范教练。请输出一个 Mermaid mindmap,主题是“LangChain Tool 接口设计规范与参数校验”。
必须包含:命名、输入字段设计、Pydantic 校验、输出结构、失败语义、trace_id、描述优化、注册与调用链路、安全与脱敏。
只输出 <<<MERMAID>>>...<<<END_MERMAID>>>,不要输出解释文字。
模板 2:生成通用工具基础代码(带参数校验)
你是 Python + LangChain 1.2.7 工具开发工程师。请为我生成一个通用工具:
- 工具名:{tool_name}
- 功能一句话:{one_line_spec}
- 输入字段:{fields_with_type_and_rules}
- 输出结构:必须返回 JSON 字符串,含 ok/data/error/trace_id
- 校验要求:类型校验 + 范围/枚举/长度 + 至少 1 条字段间关系校验
- 自测:给出 5 条用例(3 正常 + 2 异常)与运行方式
代码要求:
- 使用 langchain_core.tools 的 @tool 或 StructuredTool(任选其一,但要说明你为什么选)
- 不要引入项目里没有提到的第三方库
项目工坊主题:通用工具开发、参数校验、描述优化、注册与基础调用全流程演示与实操。
- 演示:同一个工具,描述写得“含糊 vs 具体”时,模型调用准确率的差异。
- 演示:从“手写校验”升级到“Pydantic schema”,观察错误信息如何变得更可读。
- 学生分组:每组开发 1 个通用工具(字符串处理/数值计算/数据格式转换任选)。
- 每组完成:工具自测(至少 5 用例)→ 注册到 Agent → 让 Agent 调用 2 次(正常 1 + 异常 1)。
- 工具输入有明确边界:至少 2 条校验规则(例如长度 + 枚举)。
- 输出可复验:返回 JSON 字符串,且含 trace_id。
- 有自测证据:贴出 2 条用例输出(正常/异常各 1)。
九、课程思政融入点(数据安全与质量意识)
- 工具一旦连接工业系统,“参数就是指令”,错误参数可能等于错误操作。
- 参数校验是安全底线:防止越权字段、危险字符、超长输入、非法枚举导致误操作与日志污染。
- 输出带 trace_id 是审计起点:出了问题能回放、能追责、能改进。
本次课只做一件事:把“调用失败”变成可复现、可定位、可修复、可回归的闭环。
- 参数问题:类型不匹配、枚举不在范围、缺字段、超长、字段间关系不满足。
- 注册问题:工具名冲突、工具没加入 tools 列表、工具描述太含糊导致模型不选它。
- 依赖问题:包没装、导入路径变动、环境变量未配置(Key、模型名、网络)。
1. 最小化复现:先绕开 Agent,直调 Tool
目标:先证明“工具本身”是否可靠。
from tools_common import text_transform
def minimal_repro():
print("call-1", text_transform.invoke({"text": "abc", "mode": "upper"}))
print("call-2", text_transform.invoke({"text": "abc", "mode": "oops"}))
if __name__ == "__main__":
minimal_repro()
解释与自检要点:
- 如果直调都失败,先修工具,不要急着怪 Agent 或模型。
- 直调能稳定通过,再进入“注册与选择”层面排查。
2. 断点调试(推荐在 VS Code)
建议断点位置(只选 2–3 个就够用):
- 工具函数入口:检查入参是否符合预期(是否被意外传入了 dict/None/超长)。
- 关键校验分支:确认是不是“校验过严/过松”导致误拒绝或误放行。
- 输出组装处:确认输出结构稳定,trace_id 是否存在。
命令行快速断点(无需 IDE,入门够用):
python -m pdb .\app.py
解释与自检要点:
-m pdb:进入交互式断点调试;常用命令是n(下一步)、s(进入函数)、p 变量(打印变量)、c(继续)。.\\app.py:把这里替换为你正在调试的脚本文件名(例如.\\minimal_repro.py)。- 先在“工具函数入口”打断点,最容易看清“参数到底长什么样”。
最低证据链(每个问题至少保留这些):
- 用户输入原文(或你手动构造的 payload)
- 工具名 + 入参 payload(脱敏后)
- 工具输出(含 trace_id)
- 修复点说明(改了哪个校验、为什么)
- 回归用例输出(同一问题不再出现)
1. 常见“描述太差”的表现
- 工具描述只有一句“处理文本/计算数值”,没有写“何时用、输入约束、输出结构”。
- 参数字段名含糊(
data、input),导致模型填参经常错。 - 没写长度上限/枚举,模型经常传入超长或同义词,触发校验失败。
2. 描述优化模板(直接套用)
把你们工具描述改成下面结构:
- 一句话用途:当用户要做 X 时,调用本工具。
- 入参约束:列出每个字段的类型与限制(枚举/范围/长度)。
- 输出说明:返回 JSON 字符串,含 ok/data/error/trace_id;失败时 error 描述明确。
- 注意事项:不处理敏感信息;不接受超长文本;遇到不确定必须返回错误而不是猜。
模板 1:用 AI 分析日志并给出“可执行步骤”
你是 Python + LangChain 工具调试专家。我会提供:工具代码片段、报错日志、触发输入。
请输出:
1) 先分类:这是参数问题/注册问题/依赖问题中的哪一类(可多选,但要给理由)
2) 最小复现步骤(不用 Agent,直接调用工具)
3) 精准定位点:建议打印/断点的变量与位置
4) 修复方案:给出最小改动的代码补丁(只改必要行)
5) 回归用例:至少 3 条(含 1 条异常)
输出必须按 <<<CLASSIFY>>>、<<<REPRO>>>、<<<LOCATE>>>、<<<PATCH>>>、<<<REGRESSION>>> 分段。
材料:{把你的内容粘贴在这里}
模板 2:让 AI 优化工具描述(提升可调用性)
你是 LangChain Tool 描述优化专家。请基于我的工具函数签名与校验规则,输出一段更好的 tool description。
要求:
- 不超过 80 字,但要包含触发条件 + 关键约束 + 输出结构
- 约束必须是明确的数字/枚举/字段名(不要写“尽量/大概/合适”)
材料:{工具签名、校验规则、输出示例}
教师演示案例(建议二选一):
- 案例 A:参数类型不匹配(
digits="two")导致 Pydantic/运行时校验失败,如何从日志定位并修复。 - 案例 B:工具名冲突(两个工具都叫
text_transform)导致注册异常或调用混乱,如何定位并重命名。
学生分组任务(用自己的工具做):
- 人为制造 1 个失败(参数错/描述差/注册错任选其一)。
- 复现并记录证据链。
- 修复并回归通过。
- 迭代描述文本,让模型调用更稳定。
八、课程思政融入点(质量意识与技术融合)
- AI 能提升排障效率,但不能替代质量责任;关键改动必须人工审计与复验。
- 工业 AI 系统追求“稳定与可追责”,不是“偶尔聪明”;工具描述与参数校验是稳定性工程的一部分。
作业
- 不布置